package com.gotokeep.keep.composition;

import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaMuxer;
import android.opengl.GLUtils;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;
import android.view.Surface;

import com.gotokeep.keep.composition.renderer.MediaRenderer;
import com.gotokeep.keep.composition.track.MediaTrackSet;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;

/**
 * @author xana/cuixianming
 * @version 1.0
 * @since 2018-05-03 16:13
 */
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
/* package */ class MeidaCompositionImpl implements MediaComposition, Handler.Callback {

    // external messages
    public static final int MSG_EVENT_SETUP = 0;
    public static final int MSG_EVENT_RELEASE = 1;
    public static final int MSG_EVENT_PREPARING = 2;
    public static final int MSG_EVENT_RENDER_FRAME = 3;
    public static final int MSG_EVENT_ERROR = 4;

    private static final int EXPORT_MSG_OFFSET = 100;
    public static final int MSG_EXPORT_START = EXPORT_MSG_OFFSET;
    public static final int MSG_EXPORT_FRAME = EXPORT_MSG_OFFSET + 1;
    public static final int MSG_EXPORT_MUXING = EXPORT_MSG_OFFSET + 2;
    public static final int MSG_EXPORT_COMPLETE = EXPORT_MSG_OFFSET + 3;
    public static final int MSG_EXPORT_ERROR = EXPORT_MSG_OFFSET + 4;


    private static final int MSG_SETUP = 0;
    private static final int MSG_PREPARE = 1;
    private static final int MSG_DO_RENDER = 2;
    private static final int MSG_SET_VIDEO_TRACK_SET = 3;
    private static final int MSG_START = 4;
    private static final int MSG_PAUSE = 5;
    private static final int MSG_SEEK = 6;
    private static final int MSG_EXPORT = 7;
    private static final int MSG_RELEASE = 8;
    private static final int MSG_END = 9;

    private static final int MODE_PLAY = 0;
    private static final int MODE_COMPOSING = 1;
    private static final String TAG = MediaComposition.class.getSimpleName();

    private HandlerThread handlerThread;
    private Handler handler;
    private Handler eventHandler;
    private Handler exportHandler;

    private List<WeakReference<EventListener>> eventListenerLists = new LinkedList<>();
    private EventDispatcher eventDispatcher;


    ///////////////////////////////////////////////////////////////////////////
    // OpenGL variable
    ///////////////////////////////////////////////////////////////////////////
    private EGL10 mEgl;
    private EGLDisplay mEglDisplay = EGL10.EGL_NO_DISPLAY;
    private EGLContext mEglContext = EGL10.EGL_NO_CONTEXT;
    private EGLSurface mEglSurface = EGL10.EGL_NO_SURFACE;
    private EGLConfig[] configs;

    private MediaRenderer renderers[];
    private Surface outputSurface;

    ///////////////////////////////////////////////////////////////////////////
    // Exporting variable
    ///////////////////////////////////////////////////////////////////////////
    private Surface exportVideoSurface;
    private MediaMuxer muxer;
    private boolean allTrackAdded = false;
    private boolean muxerStarted = false;
    private boolean allRenderEnd = false;

    private int renderMode = MODE_PLAY;
    private boolean verbose = false;

    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final int EGL_OPENGL_ES2_BIT = 4;

    private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
    private static final int COMPOSING_INTERVAL_MS = 0;
    private static final int RENDERING_INTERVAL_MS = 10;
    private static final int IDLE_INTERVAL_MS = 1000;

    public MeidaCompositionImpl() {
        eventDispatcher = new EventDispatcher(eventListenerLists);
        handlerThread = new HandlerThread("com.gotokeep.keep.composition.MediaCompositionImpl");
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper(), this);
    }

    @Override
    public void setup(Surface outputSurface) {
        handler.obtainMessage(MSG_SETUP, outputSurface).sendToTarget();
    }

    @Override
    public void release() {
        handler.sendEmptyMessage(MSG_RELEASE);
    }

    @Override
    public void setVideoTrackSet(MediaTrackSet trackSet) {
        handler.obtainMessage(MSG_SET_VIDEO_TRACK_SET, trackSet).sendToTarget();
    }

    @Override
    public void prepare() {
        handler.sendEmptyMessage(MSG_PREPARE);
    }

    @Override
    public void setOutputSurface(Surface previewSurface) {
        this.outputSurface = previewSurface;
    }

    @Override
    public void start() {
        handler.sendEmptyMessage(MSG_START);
    }

    @Override
    public void pause() {
        handler.sendEmptyMessage(MSG_PAUSE);
    }

    @Override
    public void seekTo(long timeUs) {
        handler.obtainMessage(MSG_SEEK, timeUs).sendToTarget();
    }

    @Override
    public void export(MediaExportConfiguration configuration) {
        handler.obtainMessage(MSG_EXPORT, configuration).sendToTarget();
    }

    @Override
    public void setExportListener(Handler exportEventHandler) {
        this.exportHandler = exportEventHandler;
    }

    public void setVerbose(boolean verbose) {
        this.verbose = verbose;
    }

    public void addEventListener(EventListener listener) {
        eventListenerLists.add(new WeakReference<>(listener));
    }

    public void removeEventListener(EventListener listener) {
        Iterator<WeakReference<EventListener>> iterator = eventListenerLists.iterator();
        while (iterator.hasNext()) {
            WeakReference<EventListener> listenerRef = iterator.next();
            if (listenerRef.get() == listener) {
                iterator.remove();
            }
        }
    }

    @Override
    public boolean handleMessage(Message msg) {
        try {
            switch (msg.what) {
                case MSG_SETUP:
                    setupInternal((Surface) msg.obj);
                    return true;
                case MSG_RELEASE:
                    releaseInternal();
                    return true;
                case MSG_DO_RENDER:
                    doRender();
                    return true;
                case MSG_START:
                    startInternal();
                    return true;
                case MSG_PAUSE:
                    pauseInternal();
                    return true;
                case MSG_SEEK:
                    seekToInternal((long) msg.obj);
                    return true;
                case MSG_EXPORT:
                    exportInternal((MediaExportConfiguration) msg.obj);
                    return true;
                case MSG_END:
                    endInternal();
                    return true;
                default:
                    // Nop
            }
        } catch (Exception e) {
            return false;
        } finally {
            msg.recycle();
        }
        return false;
    }

    private void setupInternal(Surface outputSurface) {
        this.outputSurface = outputSurface;
        setupGL(outputSurface);
    }

    private void releaseInternal() {
        releaseGL();
        releaseEncoder();
        handlerThread.quitSafely();
    }

    private void doRender() {
        long operationStartTimeMs = SystemClock.elapsedRealtime();
        // getRenderFrame?
        // getPresentationTimeUs <- renderFrame
        // endOfStream = true;
        // for (MediaRender render : renders)
        // endOfStream &= generateFrame(presentationTimeUs);

        long minWaitTimeMs = IDLE_INTERVAL_MS;
        for (MediaRenderer renderer : renderers) {
            long presentationTimeUs;
            try {
                presentationTimeUs = renderer.render(0);
            } catch (Exception e) {
                eventDispatcher.onError(e);
                return;
            }
            allRenderEnd &= renderer.isEnd();
            if (renderMode == MODE_COMPOSING) {
                try {
                    // 导出：编码（Encode）、复用（Mux）
                    if (!renderer.isEnd()) {
                        if ((!renderer.isReady()) || muxerStarted) {
                            renderer.encodeFrame();
                        }
                        if (muxerStarted) {
                            if (renderer.isReady() && !renderer.isEnd()) {
                                drainEncoder(renderer);
                            }
                        } else {
                            allTrackAdded &= ensureRendererReady(renderer);
                        }
                    }
                } catch (Exception e) {
                    if (eventHandler != null) {
                        eventHandler.obtainMessage(MSG_EVENT_ERROR, e).sendToTarget();
                    }
                }
            } else {
                minWaitTimeMs = Math.min(minWaitTimeMs, presentationTimeUs / 1000);
            }
        }
        if (renderMode == MODE_COMPOSING) {
            if (allTrackAdded && !muxerStarted) {
                muxer.start();
                muxerStarted = true;
            }
            if (allRenderEnd) {

            }
            // call next render immediately
            scheduleNextRender(operationStartTimeMs, COMPOSING_INTERVAL_MS);
        } else {
            // 播放模式
            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
            scheduleNextRender(operationStartTimeMs, RENDERING_INTERVAL_MS);
        }
    }

    private boolean ensureRendererReady(MediaRenderer renderer) {
        if (renderer.getMuxerTrackIndex() < 0 && renderer.getOutputFormat() != null) {
            int trackIndex = muxer.addTrack(renderer.getOutputFormat());
            renderer.setMuxerTrackIndex(trackIndex);
        }
        return renderer.isReady();
    }

    private void drainEncoder(MediaRenderer renderer) {
        int trackIndex = renderer.getMuxerTrackIndex();
        ByteBuffer buffer = renderer.getOutputBuffer();
        MediaCodec.BufferInfo bufferInfo = renderer.getOutputBufferInfo();

        muxer.writeSampleData(trackIndex, buffer, bufferInfo);

        if (verbose) {
            Log.v(TAG, "sent" + bufferInfo.size + " bytes to muxer");
        }
        renderer.releaseOutputBuffer();
    }

    private void startInternal() {

    }

    private void pauseInternal() {

    }

    private void seekToInternal(long positionUs) {

    }

    private void exportInternal(MediaExportConfiguration configuration) {
        try {
            setupEncoder(configuration);
            renderMode = MODE_COMPOSING;
            handler.sendEmptyMessage(MSG_DO_RENDER);
        } catch (IOException e) {
            if (exportHandler != null) {
                exportHandler.obtainMessage(MSG_EXPORT_ERROR, e).sendToTarget();
            }
        }
    }

    private void endInternal() {

    }

    private void scheduleNextRender(long thisOperationStartTimeMs, long intervalMs) {
        handler.removeMessages(MSG_DO_RENDER);
        long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
        long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
        if (nextOperationDelayMs <= 0) {
            handler.sendEmptyMessage(MSG_DO_RENDER);
        } else {
            handler.sendEmptyMessageDelayed(MSG_DO_RENDER, nextOperationDelayMs);
        }
    }

    private void setupGL(Surface outputSurface) throws RuntimeException {
        mEgl = (EGL10) EGLContext.getEGL();

        mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetDisplay failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        int[] version = new int[2];
        if (!mEgl.eglInitialize(mEglDisplay, version)) {
            throw new RuntimeException("eglInitialize failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        int[] configAttribs = {
                EGL10.EGL_BUFFER_SIZE, 32,
                EGL10.EGL_ALPHA_SIZE, 8,
                EGL10.EGL_BLUE_SIZE, 8,
                EGL10.EGL_GREEN_SIZE, 8,
                EGL10.EGL_RED_SIZE, 8,
                EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
                EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
                EGL10.EGL_NONE
        };

        int[] numConfigs = new int[1];
        configs = new EGLConfig[1];
        if (!mEgl.eglChooseConfig(mEglDisplay, configAttribs, configs, 1, numConfigs)) {
            throw new RuntimeException("eglChooseConfig failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        int[] contextAttribs = {
                EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL10.EGL_NONE
        };
        mEglContext = mEgl.eglCreateContext(mEglDisplay, configs[0], EGL10.EGL_NO_CONTEXT, contextAttribs);
        mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, configs[0], outputSurface, null);
        if (mEglSurface == EGL10.EGL_NO_SURFACE || mEglContext == EGL10.EGL_NO_CONTEXT) {
            int error = mEgl.eglGetError();
            if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
                throw new RuntimeException("eglCreateWindowSurface returned  EGL_BAD_NATIVE_WINDOW. ");
            }
            throw new RuntimeException("eglCreateWindowSurface failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }

        if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
            throw new RuntimeException("eglMakeCurrent failed : " +
                    GLUtils.getEGLErrorString(mEgl.eglGetError()));
        }
    }

    private void releaseGL() {
        if (mEgl != null) {
            mEgl.eglDestroyContext(mEglDisplay, mEglContext);
            mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
            mEgl = null;
        }
        mEglContext = EGL10.EGL_NO_CONTEXT;
        mEglSurface = EGL10.EGL_NO_SURFACE;
    }

    private void setupEncoder(MediaExportConfiguration configuration) throws IOException {
        for (MediaRenderer renderer : renderers) {
            renderer.prepareEncoder(configuration);
        }

        muxer = new MediaMuxer(configuration.getFilePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        muxerStarted = false;
        allTrackAdded = false;
        allRenderEnd = false;
    }

    private void releaseEncoder() {
        if (verbose) {
            Log.v(TAG, "releasing encoders");
        }
        for (MediaRenderer renderer : renderers) {
            renderer.releaseEncoder();
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && muxer != null) {
            muxer.stop();
            muxer.release();
            muxer = null;
        }
        muxerStarted = false;
        allTrackAdded = false;
        allRenderEnd = false;
    }

    private static class EventDispatcher implements EventListener {
        private List<WeakReference<EventListener>> eventListenerLists;

        private EventDispatcher(List<WeakReference<EventListener>> eventListenerLists) {
            this.eventListenerLists = eventListenerLists;
        }

        @Override
        public void onSetup() {
            notifyAllEventListener(new EventDispatcherAction() {
                @Override
                public void dispatch(EventListener listener) {
                    listener.onSetup();
                }
            });
        }

        @Override
        public void onReleased() {
            notifyAllEventListener(new EventDispatcherAction() {
                @Override
                public void dispatch(EventListener listener) {
                    listener.onReleased();
                }
            });
        }

        @Override
        public void onPreparing() {
            notifyAllEventListener(new EventDispatcherAction() {
                @Override
                public void dispatch(EventListener listener) {
                    listener.onPreparing();
                }
            });
        }

        @Override
        public void onRenderedFrame(final int frame, final long timeUs) {
            notifyAllEventListener(new EventDispatcherAction() {
                @Override
                public void dispatch(EventListener listener) {
                    listener.onRenderedFrame(frame, timeUs);
                }
            });
        }

        @Override
        public void onError(final Exception e) {
            notifyAllEventListener(new EventDispatcherAction() {
                @Override
                public void dispatch(EventListener listener) {
                    listener.onError(e);
                }
            });
        }

        private void notifyAllEventListener(EventDispatcherAction action) {
            if (action == null) {
                return;
            }
            Iterator<WeakReference<EventListener>> iterator = eventListenerLists.iterator();
            while (iterator.hasNext()) {
                WeakReference<EventListener> listenerRef = iterator.next();
                if (listenerRef != null && listenerRef.get() != null) {
                    EventListener listener = listenerRef.get();
                    action.dispatch(listener);
                } else {
                    iterator.remove();
                }
            }
        }
    }

    private interface EventDispatcherAction {
        void dispatch(EventListener listener);
    }
}
